#primerpy version 0.91, codename huckleberry
#
"""
#This huckleberry branch takes a minimalist approach
#	the idea is to do everything in one file with built-in Python modules
#	so that users don't need extra installation besides primers
#	this version runs but lack some error handling
#The next branch will abandon Tk and change to wxWidgets (codename tortilla)
#branch huckleberry will no longer be maintained.

# How to use:
# PrimerPy ships with only two files: primerpy.py (this program) and primerpy_doc.txt
# copy these files into the directory of primer3 installation, something like primer3_1.0b/
# In a *NIX shell, run "python primerpy.py" to evoke this program and GUI
# For Windows, double click may do the trick; but consult primerpy_doc.txt if needed
# paste your template sequence into the GUI, click "design"
"""
from Tkinter import *
import tkMessageBox
import time, os, sys
from subprocess import *

PrimerPyVersion="v0.91"
# version update, change os.popen to subprocess


# modify the following two variables if desired
# ------------------------------------------------------------------------------ #
default_text_editor='kate' # the default *NIX text editor,e.g. gedit for Gnome
# On Windows, PrimerPy uses default ".txt" editor 
resultspath="results/" # the directory where you want to save results
# ------------------------------------------------------------------------------ #
if os.path.exists(resultspath) == False:
    os.system('mkdir '+resultspath)

execorepath="src/primer3_core -format_output"
run_time_ID='RUN_@_' + time.asctime().replace(" ", "_") + ' >>>'
run_number=0
tmpinput="tmpPrimer3coreInputFile"
pcmd = "./" + execorepath + " < " + tmpinput
LogFile=resultspath+"PrimerPy_log.txt"
DocumentationName="primerpy_doc.txt"
if "win" in sys.platform: # if on MS Windows 
    execorepath="primer3 -format_output"
    pcmd = execorepath + "< " + tmpinput


# ---------------------------------------------------------------------------------------------------------------
def boulder_write(dict):
     "write text stream in BoulderIO format from dictionary"
     b=""
     for key, value in dict.items():
         b += key+'='+str(value)+'\n'
     b+='=\n' #trailing "=" in BoulderIO file
     return b

def write_log(input_str, result):
    f=open(LogFile, 'a')
    f.write('\n#-------------------------------------------------------\n'+run_time_ID+'\n\n')
    f.write('===The input was===\n\n'+input_str)
    f.write('===The result was===\n\n'+result+'\n\n')
    f.close()

def display_file(input_file):
    if "win" in sys.platform:
        os.startfile(input_file)
    else:
        os.system(default_text_editor+' '+input_file+' &')

# ---------------------------------------------------------------------------------------------------------------
class InputDict:
    """class InputDict takes initial parameters and those from GUI input"""
    def __init__(self, template='', left_primer='', right_primer='', amplicon_size=''):
        self.initial_dict={"PRIMER_PICK_ANYWAY" : 1,
                      "PRIMER_PRODUCT_MAX_TM" : 90,
                      "PRIMER_PRODUCT_MIN_TM" : 75,
                      "PRIMER_PRODUCT_SIZE_RANGE" : "90-150 80-300",
                      "PRIMER_MIN_SIZE" : 18,
                      "PRIMER_MAX_SIZE" : 22,
                      "PRIMER_MIN_TM" : 59,
                      "PRIMER_MAX_TM" : 61,
                      "PRIMER_MIN_GC" : 45,
                      "PRIMER_MAX_GC" : 55,
                      "PRIMER_SELF_ANY" : 3,
                      "PRIMER_SELF_END" : 0,
                      "PRIMER_NUM_RETURN" : 3}
        self.GUI_dict={}

    def gettemplate(self, text):
        text=text.lstrip()
        text=text.replace(" ", "")
        if text == "":
            return 0
        else:
            if text[0]==">":
                firstline=text.find('\n')
                if firstline < 0:
                    firstline=text.find('\r')
                self.GUI_dict['PRIMER_SEQUENCE_ID']=text[ :firstline]
                text=text[firstline+2: ]
            else:
                self.GUI_dict['PRIMER_SEQUENCE_ID']=run_time_ID
            self.GUI_dict['SEQUENCE']=text.replace("\n", "")
            return 1

    def update_parameters(self, left, right, size, note):
        if note.rstrip() != "":
            self.GUI_dict['Users_Note']=note.replace(" ", "")
        else:
            self.GUI_dict['Users_Note']="RUN"
        if left.rstrip() != "":
            self.GUI_dict['PRIMER_LEFT_INPUT'] = left.replace(" ", "")
        if right.rstrip() != "":
            self.GUI_dict['PRIMER_RIGHT_INPUT'] = right.replace(" ", "")
        if size.rstrip() != "":
            self.GUI_dict['PRIMER_PRODUCT_OPT_SIZE'] = size.replace(" ", "")


# ---------------------------------------------------------------------------------------------------------------
class ThinWrapper:
    """ThinWrapper class takes care of running primer3_core,
                      feeding input and collect output; if run succeeds, write output into result file and log file
                      (primer3_core seems to take only files not stdin, is there better way than using on disk files?)"""
    def write_input(self, input_str):
        myhandle=open(tmpinput, 'w')
        myhandle.write(input_str)
        myhandle.close()
    def write_output(self, usersnote, output_str):
        self.runresult=resultspath+'_'.join([usersnote, run_time_ID[4:-4].replace(":", ""), str(run_number)])+'.txt'
        myhandle=open(self.runresult, 'w')
        myhandle.write(output_str)
        myhandle.close()
    def call_primer3_core(self, input_dict):
        input_str = boulder_write(input_dict)
        self.write_input(input_str)
	p=Popen(pcmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
        (primer3_in, primer3_out, primer3_err)=(p.stdin, p.stdout, p.stderr)
	#(primer3_in, primer3_out, primer3_err)=os.popen3(pcmd)
        primer3_out_text = primer3_out.read()
        if primer3_out_text.rstrip() == "":
            print primer3_err
            write_log(input_str, primer3_err.read())
            return 1
        else:
            if "NO PRIMERS FOUND" not in primer3_out_text[:500]:
                self.write_output(input_dict["Users_Note"], primer3_out_text)
                print "Things look good. Result was written to:\n", self.runresult.replace(resultspath, ""), "\n"
                write_log(input_str, 'saved in '+self.runresult)
                display_file(self.runresult)
                return 1
            else:
                print "Revising parameters... Try again...\n"
                return 0
    def logic_loop(self, input_dict):
        result=self.call_primer3_core(input_dict)
        if result != 1:
            input_dict["PRIMER_SELF_ANY"]=4
            result=self.call_primer3_core(input_dict)
            if result != 1:
                input_dict["PRIMER_MIN_TM"]=58
                input_dict["PRIMER_MAX_TM"]=62
                result=self.call_primer3_core(input_dict)
                if result != 1:
                    input_dict["PRIMER_MIN_GC"]=40
                    input_dict["PRIMER_MAX_GC"]=60
                    result=self.call_primer3_core(input_dict)
                    if result != 1:
                        input_dict["PRIMER_MIN_GC"]=30
                        input_dict["PRIMER_MAX_GC"]=70
                        result=self.call_primer3_core(input_dict)
                        if result != 1:
                            input_dict["PRIMER_SELF_END"]=1
                            result=self.call_primer3_core(input_dict)
                            if result != 1:
                                print "That's how far I'd go. It seems you may not have a primer good for SG QPCR here :("
                                tkMessageBox.showinfo(title="Ohoh...", message="You may not have good QPCR primers here.\
                                                      You can try the 'no luck?' button to design primers under loose conditions.\
                                                      But we do NOT recommend using such primers in QPCR. \
                                                      Good Luck!")
    def noluck(self, input_dict):
        loose_dict={"PRIMER_PICK_ANYWAY" : 1,
                      "PRIMER_PRODUCT_MAX_TM" : 93,
                      "PRIMER_PRODUCT_MIN_TM" : 75,
                      "PRIMER_PRODUCT_SIZE_RANGE" : "80-300 80-700",
                      "PRIMER_MIN_SIZE" : 17,
                      "PRIMER_MAX_SIZE" : 25,
                      "PRIMER_MIN_TM" : 56,
                      "PRIMER_MAX_TM" : 64,
                      "PRIMER_MIN_GC" : 30,
                      "PRIMER_MAX_GC" : 70,
                      "PRIMER_SELF_ANY" : 8,
                      "PRIMER_SELF_END" : 8,
                      "PRIMER_NUM_RETURN" : 3}
        input_dict.update(loose_dict)
        result=self.call_primer3_core(input_dict)
        if result != 1:
            print "Oh dear, you are really out of luck!"
            tkMessageBox.showinfo(title="No primers found...", message="Oh dear, you are really out of luck!\
                    Have another piece of sequence?")


# ---------------------------------------------------------------------------------------------------------------
class ScrolledText(Frame):
    """ ScrolledText class adopted from Mark Lutz, for GUI construction
                used for template input textbox with scroll bar"""
    def __init__(self, master = None, text=' '):
        Frame.__init__(self, master)
        self.pack(expand=YES)
        self.makewidgets()
        self.settext(text)
    def makewidgets(self):
        sbar=Scrollbar(self)
        text=Text(self, height=12, width=55, relief=SUNKEN, bg='yellow', fg='blue')
        sbar.config(width=16, command=text.yview)
        text.config(yscrollcommand=sbar.set)
        sbar.pack(side=LEFT, fill=Y)
        text.pack(side=LEFT, expand=YES, fill=BOTH)
        self.text=text
    def settext(self, text=' '):
        self.text.delete('1.0', END)
        self.text.insert('1.0', text)
        self.text.mark_set(INSERT, '1.0')
        self.text.focus()
    def gettext(self):
        return self.text.get('1.0', END+'-1c')

# ---------------------------------------------------------------------------------------------------------------
class ParameterArea(Frame):
    """ Parameter Area, including
         Users' Note, Amplicon Size Desired, Designated Left Primer Sequence, Designated Right Primer Sequence"""
    def __init__(self, master = None, text=' '):
        Frame.__init__(self, master)
        self.pack(expand=YES)
        self.parameters()
    def parameters(self):
        Label(self, text="User's Note:",  fg='#387b17').grid(row=0, column=0, sticky=N+E)
        self.UsersNote=Entry(self, fg='blue')
        self.UsersNote.grid(row=0, column=1, stick=N+W)
        Label(self, text="Amplicon Size Desired:", fg='#387b17').grid(row=0, column=2, sticky=N+E)
        self.AmpliconSize=Entry(self, width=5, fg='blue')
        self.AmpliconSize.grid(row=0, column=3, sticky=N+W)
        Label(self, text="You Can Specify Left Primer Here", fg='#387b17').grid(row=1, column=0, columnspan=2, sticky=N+W)
        Label(self, text="You Can Specify Right Primer Here", fg='#387b17').grid(row=1, column=2, columnspan=2, sticky=N+W)
        self.LeftPrimer=Entry(self, width=28, fg='blue')
        self.RightPrimer=Entry(self, width=28, fg='blue')
        self.LeftPrimer.grid(row=2, column=0, columnspan=2, sticky=N+W)
        self.RightPrimer.grid(row=2, column=2, columnspan=2, sticky=N+W)



###################################################################
# Main GUI class for PrimerPy
# instances from class InputDict, ThinWrapper and ScrolledText
###################################################################
class PrimerPyInterface(Frame):
    """ construction of main GUI for PrimerPy """
    def __init__(self, master = None):
        Frame.__init__(self, master)
        #self.grid(ipadx=8)
        self.pack(ipadx=8)
        self.headerLabel()
        self.createTxTemplate()
        self.createParaArea()
        self.actionButtons()
        self.bottomButtons()
        self.statusBar("Welcome. Please read our short Documentation if you try PrimerPy the first time.")
        # make instance of inputDict, ThinWrapper
        self.BoulderDict=InputDict()
        self.wrapper=ThinWrapper()

    def headerLabel(self):
        Label(self, text="PrimerPy", fg='red', font=('courier', 20, 'bold'), height=2).grid(row=0, column=0, columnspan=1, sticky=W+S)
        Label(self, text="Primer3 tuned for SG QPCR", fg='red', font=('times', 12, 'italic'), height=2).grid(row=0, column=1, columnspan=3, sticky=W+S)
        Button(self, text='Paste Template Sequence Here  ', relief=GROOVE, command=self.onPaste).grid(row=2, column=0, columnspan=2, sticky=W+E)
        Button(self, text='Clear All', relief=GROOVE, command=self.clearAll).grid(row=2, column=2, sticky=W+E)
        
    def createTxTemplate(self):
        self.TxTemplate=ScrolledText(master=self)
        self.TxTemplate.grid(row=3, rowspan=8, column=0, columnspan=4)
        self.TxTemplate.text.bind("<Control-KeyPress-v>", self.onPaste)

    def createParaArea(self):
        self.ParaArea=ParameterArea(master=self)
        self.ParaArea.grid(row=11, rowspan=3, column=0, columnspan=4)

    def actionButtons(self):
        self.DesignButton = Button(self, text="Design", fg='#4043db', font=('times', 24, 'bold'), width=9, height=4, command=self.strictDesign)
        self.DesignButton.grid(row=2, rowspan=6, column=4, columnspan=2, sticky=N)
        self.NoluckButton = Button(self, text="no luck?", fg='#96ccea', font=('times', 24, 'bold'), width=9, height=4, command=self.looseDesign)
        self.NoluckButton.grid(row=9, rowspan=6, column=4, columnspan=2, sticky=N)

    def bottomButtons(self):
        self.ViewLog = Button(self, text="View log file", relief=GROOVE, command=self.viewlog)
        self.ViewLog.grid(row=16, column=0, sticky=E+W)
        self.OpenFile = Button(self, text = "View last run", relief=GROOVE, command=self.viewrun)
        self.OpenFile.grid(row=16, column=1, sticky=E+W)
        self.manual = Button(self, text="Documentation", relief=GROOVE, command=self.manual)
        self.manual.grid(row=16, column=2, sticky=E+W)
        self.About = Button(self, text="About", relief=FLAT, command=self.about)
        self.About.grid(row=16, column=3, sticky=E+W)

    def statusBar(self, mytext):
        self.statusinfo=Label(self, text=mytext, fg='blue', bd=1, anchor=W, relief=SUNKEN) 
        self.statusinfo.grid(row=17, column=0, columnspan=6, sticky=W+E)

    def onPaste(self, *ev):
        try:
            ptext=self.selection_get(selection='CLIPBOARD')
            self.TxTemplate.text.insert(INSERT, ptext)
        except TclError:
            self.statusinfo.configure(text='Paste error. Please copy your sequence into Clipboard first')
        
    def clearAll(self):
        self.TxTemplate.text.delete('1.0', END+'-1c')
        self.ParaArea.LeftPrimer.delete(0, END)
        self.ParaArea.RightPrimer.delete(0, END)
        self.ParaArea.UsersNote.delete(0, END)
        self.ParaArea.AmpliconSize.delete(0, END)

    def designAction(self):
        global run_number, run_time_ID
        run_number+=1
        run_time_ID='RUN_@_' + time.asctime().replace(" ", "_") + ' >>>'
        template=self.TxTemplate.gettext()
        left_primer=self.ParaArea.LeftPrimer.get()
        right_primer=self.ParaArea.RightPrimer.get()
        amplicon_size=self.ParaArea.AmpliconSize.get()
        usernote=self.ParaArea.UsersNote.get()
        checktemplate=self.BoulderDict.gettemplate(template)
        if checktemplate:
            self.BoulderDict.update_parameters(left_primer, right_primer, amplicon_size, usernote)
            self.BoulderDict.GUI_dict.update(self.BoulderDict.initial_dict)
            self.statusinfo.configure(text='Please wait while Primer3 is running ........ ')
            return 1
        else:
            self.statusinfo.configure(text='No template!')
            return 0 

    def strictDesign(self):
        testinputs=self.designAction()
        if testinputs:
            self.wrapper.logic_loop(self.BoulderDict.GUI_dict)
            self.statusinfo.configure(text='Done ... ')
    def looseDesign(self):
        testinputs=self.designAction()
        if testinputs:
            self.wrapper.noluck(self.BoulderDict.GUI_dict)
            self.statusinfo.configure(text='Result of this run is UNLIKELY to work in QPCR. Use with precaution.')
        
    def about(self):
        self.statusinfo.configure(text='PrimerPy %s, by Shuzhao Li & Alex Pozhitkov, free under Python license' % PrimerPyVersion)
        tkMessageBox.showinfo(title="About PrimerPy",
                              message="PrimerPy %s, \nby Shuzhao Li & Alex Pozhitkov, \nfree under Python license \nhttp://www.bioinformatics.org/primerpy" % PrimerPyVersion)
    def viewrun(self):
        if run_number==0:
            self.statusinfo.configure(text='No last run was carried out?')
        else:
            display_file(self.wrapper.runresult)
    def manual(self):
        display_file(DocumentationName)
    def viewlog(self):
        display_file(LogFile)
        
        
###################################################################
# main
###################################################################
root=Tk()
app = PrimerPyInterface(root)
app.master.title("PrimerPy")
root.wm_resizable(0,0)
app.mainloop()

